5.05. Работа с типами
Работа с числами
Числа — основа вычислений. Но не все числа одинаковы. В C# есть разные типы чисел, каждый со своими особенностями точности, производительности и назначения. Выбор неправильного типа может привести к ошибкам округления, переполнению или финансовым потерям. float, double, decimal — все они дробные, но работают совершенно по-разному.
float имеет точность ~6–7 значащих цифр и подходит для графики, физики, играх — где важна скорость, а не точность. Хранится в формате IEEE 754 (binary floating-point).
double имеет точность ~15–16 значащих цифр и является стандартным выбором для научных, инженерных или математических расчётов. Тоже binary floating-point, но точнее, чем float.
И как можно понять, деньги требуют максимальной точности.
Поэтому для них используем именно decimal, с точностью 28–29 значащих цифр. Хранится decimal в десятичном представлении (base-10), что может быть критичным для бухгалтерии. decimal медленнее, чем float/double, но точнее. И именно его нужно использовать для финансов, денег, налогов, цен.
Для обычных целых чисел, индексов и счётчиков можно использовать int.
Различия типов приносят забавную магию. Например, ошибки округления и сравнение чисел - проблема 0.1 + 0.2 != 0.3:
double a = 0.1;
double b = 0.2;
Console.WriteLine(a + b == 0.3); // False!
Почему? Потому что 0.1 не может быть точно представлено в двоичной системе (как 1/3 = 0.333... в десятичной).
Чтобы такую проблему решиь, нужно сравнивать с эпсилоном:
double epsilon = 1e-10;
bool equals = Math.Abs(a + b - 0.3) < epsilon; // True
Собственно, как правильно округлять числа? C# предоставляет несколько способов округления:
| Метод | Описание | Пример |
|---|---|---|
Math.Round(x) | Округляет к ближайшему целому | Math.Round(3.7) → 4 |
Math.Round(x, digits) | Округляет с указанным количеством знаков после запятой | Math.Round(3.14159, 2) → 3.14 |
Math.Floor(x) | Округляет вниз к наименьшему целому | Math.Floor(3.9) → 3 |
Math.Ceiling(x) | Округляет вверх к наибольшему целому | Math.Ceiling(3.1) → 4 |
Math.Truncate(x) | Отбрасывает дробную часть (без округления) | Math.Truncate(-3.7) → -3 |
По умолчанию Math.Round использует «банковское округление» (то есть к чётному):
Math.Round(2.5); // → 2 (а не 3!)
Math.Round(3.5); // → 4
Почему? Чтобы избежать систематической погрешности при массовых расчётах.
Как округлять «как в школе»?
Math.Round(2.5, MidpointRounding.AwayFromZero); // → 3
Класс Math — математика в C#.
System.Math — статический класс, содержащий методы и константы для математических операций.
Основные методы класса Math:
| Метод | Описание | Пример |
|---|---|---|
Math.Abs(x) | Возвращает абсолютное значение числа | Math.Abs(-5) → 5 |
Math.Sign(x) | Возвращает знак числа: -1 (если < 0), 0 (если = 0), 1 (если > 0) | Math.Sign(-10) → -1 |
Math.Log(x) | Натуральный логарифм (по основанию e) | Math.Log(Math.E) → 1 |
Math.Max(a, b) | Возвращает большее из двух чисел | Math.Max(10, 20) → 20 |
Math.Min(a, b) | Возвращает меньшее из двух чисел | Math.Min(10, 20) → 10 |
Math.Pow(x, y) | Возводит число x в степень y | Math.Pow(2, 3) → 8 |
Math.Sqrt(x) | Возвращает квадратный корень из числа | Math.Sqrt(16) → 4 |
Math.PI | Константа π (приблизительно 3.14159) | double circle = 2 * Math.PI * radius; |
Math.Sin(x), Math.Cos(x), Math.Tan(x) | Тригонометрические функции (аргумент в радианах) | Math.Sin(Math.PI / 2) → 1 |
Math.IEEERemainder(x, y) | Остаток от деления по стандарту IEEE (более точный, чем оператор %) | Math.IEEERemainder(5, 3) → -1 |
При ошибках вычислений могут появляться специальные значения:
double.NaN, возникает при 0.0 / 0.0, Math.Sqrt(-1) и проверяется через double.IsNaN(x). double.PositiveInfinity, возникает при 1.0 / 0.0, проверяется через double.IsInfinity(x). double.NegativeInfinity, возникает при -1.0 / 0.0, проверяется через double.IsNegativeInfinity(x). NaN != NaN — даже сравнение NaN == NaN возвращает false.
А теперь подумаем о переполнениях. Представьте, что у нас есть int. Если мы применим int.MaxValue, то заполним переменную максимально возможным для типа int целым числом. А что если попытаться прибавить единицу?
int a = int.MaxValue;
a++; // → int.MinValue (переполнение, но без ошибки)
Ответ - переполнение. Но по умолчанию ошибок не возникнет.
Поэтому нужно для такого случая делать проверяемое исключение - checked - это снижает производительность, но будет выполняться проверка на переполнение:
checked
{
int a = int.MaxValue;
a++; // Исключение: OverflowException
}
И разумеется, с числами можно работать и арифметически - но думаю, плюсы минусы, умножение, деление можно не разбирать, поэтому пойдёмте дальше.
Работа со строками
В C# строки представлены типом string (алиас для System.String) — неизменяемым ссылочным типом, который требует особого подхода к манипуляциям, сравнению и производительности.
string — это последовательность символов (char), предназначенная для хранения текста. Это ссылочный тип, но ведёт себя как значимый благодаря неизменяемости и синтаксическому удобству.
Ключевая особенность string — он неизменяем (immutable). Это означает, что как только строка создана, изменить её нельзя, и любая операция, которая «изменяет» строку, например, Replace, ToUpper, конкатенация, создаёт новый объект в куче. Пример:
string s = "hello";
s = s + " world"; // На самом деле: создаётся НОВАЯ строка
В памяти создаётся строка "hello" в куче, затем создаётся строка "hello world" в куче, и переменная s теперь ссылается на новую строку. А старая строка "hello" становится "мусором" (будет удалена GC). Такая неизменяемость безопасна в многопоточности (не нужно синхронизировать), строки можно кэшировать, и всегда есть гарантия, что строка не изменится. При множественных изменениях можно заметить недостаток - низкая производительность.
Соединение строк, или конкатенация — одна из самых частых операций. Здесь она, как обычно, через +:
string result = "Hello" + " " + name + "!";
Каждый + создаёт новый объект, так что внимательнее в циклах.
Есть и аналог в методе - string.Concat, но может быть чуть эффективнее при большом количестве аргументов.
string result = string.Concat("Hello", " ", name, "!");
Когда вы много раз изменяете строку (например, в цикле), используйте System.Text.StringBuilder. Это специальный класс, который нужен для производительности, он изменяемый, хранит содержимое в буфере, который можно расширять, и избегает создания множества временных объектов. К примеру, мы в цикле медленно будем выполнять такую операцию:
string result = "";
for (int i = 0; i < 1000; i++)
{
result += i.ToString() + ", "; // 1000 объектов в куче!
}
Но можно сделать это быстро с использованием StringBuilder:
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i).Append(", ");
}
string result = sb.ToString();
Основные методы StringBuilder:
- Append(text) добавляет текст в конец;
- AppendLine(text) добавляет текст + перевод строки;
- Insert(index, text) вставляет текст в указанную позицию;
- Remove(start, length) удаляет часть строки;
- Replace(old, new) заменяет подстроку;
- Clear() очищает содержимое;
- ToString() возвращает итоговую строку.
Поэтому в циклах с 5+ итераций, при построении длинных строк (логи, HTML, CSV), и если заранее неизвестна длина строки, используйте StringBuilder. Если знаете примерный размер, можете указывать его в начале:
var sb = new StringBuilder(1024);
Следующая операция - интерполяция строк. Это самый удобный способ форматирования строк, и работает с любыми выражениями:
string message = $"Привет, {name}! Тебе {age} лет.";
string result = $"Результат: {a + b * 2}";
double price = 19.99;
string s = $"Цена: {price:C}"; // → "Цена: $19.99" (в зависимости от локали)
{x:C}- денежный формат;{x:N2}- число с 2 знаками;{x:F1}- фиксированное число знаков;{x:D6}- целое, минимум 6 цифр;{x:yyyy-MM-dd}- дата.
Интерполяция — это синтаксический сахар над string.Format, это более старый формат. Он полезен, когда нужно переиспользовать шаблон, интерполяция недоступна или строки хранятся в ресурсах (например, .resx).
string message = string.Format("Привет, {0}! Тебе {1} лет.", name, age);
String (System.String) – неизменяемый тип для работы со строками. И для работы есть основные методы:
| Метод | Описание | Пример |
|---|---|---|
s.Length | Возвращает длину строки | "hello".Length → 5 |
s.ToUpper() | Переводит строку в верхний регистр | "abc".ToUpper() → "ABC" |
s.ToLower() | Переводит строку в нижний регистр | "ABC".ToLower() → "abc" |
s.Contains("text") | Проверяет, содержится ли подстрока в строке | "hello world".Contains("world") → true |
s.StartsWith("pre") | Проверяет, начинается ли строка с указанного текста | "apple".StartsWith("app") → true |
s.EndsWith("end") | Проверяет, заканчивается ли строка указанным текстом | "apple".EndsWith("ple") → true |
s.IndexOf("x") | Возвращает индекс первого вхождения символа или подстроки; если не найдено — -1 | "banana".IndexOf('a') → 1 |
s.LastIndexOf("x") | Возвращает индекс последнего вхождения символа или подстроки | "banana".LastIndexOf('a') → 5 |
s.Replace("old", "new") | Заменяет все вхождения указанной подстроки на новую | "hello world".Replace("world", "C#") → "hello C#" |
s.Substring(start, length) | Возвращает подстроку длиной length, начиная с позиции start | "hello world".Substring(6, 5) → "world" |
s.Split(' ') | Разбивает строку на массив строк по указанному разделителю | "one two three".Split(' ') → ["one", "two", "three"] |
s.Trim() | Удаляет пробелы (и другие whitespaces) в начале и конце строки | " text ".Trim() → "text" |
TrimStart(), TrimEnd() | Удаляет пробелы только в начале (TrimStart) или только в конце (TrimEnd) | " text ".TrimStart() → "text " |
string.Format("{0} {1}", a, b) | Форматирует строку, подставляя аргументы в позиции {0}, {1} и т.д. | string.Format("Name: {0}, Age: {1}", name, age) |
$"{name}, {age}" | Интерполяция строк — позволяет вставлять выражения прямо в строку | var s = $"Hello {name}" |
string.IsNullOrEmpty(s) | Возвращает true, если строка null или "" | string.IsNullOrEmpty(name) |
string.IsNullOrWhiteSpace(s) | Возвращает true, если строка null, пустая или содержит только пробельные символы | string.IsNullOrWhiteSpace(input) |
В C# сравнение строк тоже имеет особенности.
C# переопределил == для string, чтобы сравнивать содержимое, а не ссылки.
string a = "hello";
string b = "hello";
bool eq = a == b; // true (сравнение содержимого, а не ссылок!)
Для более гибкого сравнения используют .Equals():
bool eq = a.Equals(b); // по умолчанию — с учётом регистра
bool ignoreCase = a.Equals(b, StringComparison.OrdinalIgnoreCase);
string.Compare() - сравнение с указанием параметров:
int result = string.Compare(a, b, StringComparison.OrdinalIgnoreCase);
// result: 0 — равны, <0 — a < b, >0 — a > b
Соответственно, типы сравнения бывают:
- Ordinal - быстрое, побайтовое, по умолчанию;
- OrdinalIgnoreCase - быстрое, без учёта регистра;
- CurrentCulture - с учётом локали (например, ё в русском языке);
- CurrentCultureIgnoreCase - без учёта регистра, с учётом локали.
Для технических сравнений (ключи, файлы, URL) — Ordinal или OrdinalIgnoreCase. Для отображения пользователю — CurrentCulture.
Перебор строки используется через foreach char in str. Строка — это последовательность char, её можно перебирать:
string text = "C#";
foreach (char c in text)
{
Console.WriteLine(c); // C, # (как один символ)
}
char — 16-битный, поддерживает Unicode (UTF-16), но не все символы — один char (например, эмодзи — два char — surrogate pair).
Есть также такое понятие, как «сырая строка» (verbatim string), которая игноррирует escape-символы (\), разрешает переносы строк:
string path = @"C:\Users\John\Documents";
Для многострочных строк добавляйте ещё кавычки (начиная с C#11):
string query = """
SELECT * FROM Users
WHERE Age > 18
ORDER BY Name
""";
Этот способ автоматически удаляет отступы, что удобно для SQL, JSON, HTML. Там можно использовать интерполяцию, если добавить $ перед кавычками:
string msg = $"""
Привет, {name}!
Ты {age} лет.
""";
Поэтому для объединения используйте интерполяцию или конкатенацию, а если их много - StringBuilder. Для форматирования, хранения шаблонов используйте string.Format или интерполяцию. Не мутируйте строки - старайтесь держаться принципов и создавайте новые.
Работа с датой и временем
В повседневной жизни мы говорим «сегодня», «в 15:30», «2025-04-05». В программировании важно понимать, что дата (Date) это только день, месяц, год, время (Time) это часы, минуты, секунды. А дата-время (DateTime) это полная метка - дата + время + (опционально) информация о часовом поясе.
В C# нет отдельных типов Date или Time. Есть:
- DateTime — основной тип для даты и времени;
- DateOnly и TimeOnly — появились в .NET 6;
- DateTimeOffset — для точной привязки к UTC;
- TimeSpan — для измерения продолжительности.
DateTime — это структура, представляющая момент времени в диапазоне от 01.01.0001 до 31.12.9999. Основные свойства:
DateTime now = DateTime.Now;
Console.WriteLine(now.Year); // 2025
Console.WriteLine(now.Month); // 4
Console.WriteLine(now.Day); // 5
Console.WriteLine(now.Hour); // 14
Console.WriteLine(now.Minute); // 30
Console.WriteLine(now.Second); // 5
Console.WriteLine(now.Millisecond); // 123
Поэтому, чтобы получить текущее время, можно использовать один из трёх вариантов:
- DateTime.Now - текущее локальное время (с учётом часового пояса системы);
- DateTime.UtcNow - текущее время в UTC (всемироное скоординированное время);
- DateTime.Today - только дата (время = 00:00:00).
Рекомендуется всегда использовать UtcNow для хранения и сравнения времени в приложениях.
Важно отметить, что DateTime имеет свойство Kind, которое указывает, как интерпретировать эту дату - это тип времени, DateTime.Kind:
- DateTimeKind.Unspecified - неизвестно (по умолчанию);
- DateTimeKind.Local - локальное время (часовой пояс системы);
- DateTimeKind.Utc - время в UTC.
DateTime local = DateTime.Now; // Kind = Local
DateTime utc = DateTime.UtcNow; // Kind = Utc
DateTime unspecified = new DateTime(2025, 4, 5); // Kind = Unspecified
DateTime не хранит информацию о часовом поясе, только Kind.
Чтобы получить точное время с UTC-смещением, используется DateTimeOffset.
DateTimeOffset — это улучшенная версия DateTime, которая всегда включает смещение от UTC.
DateTimeOffset dto = new DateTimeOffset(2025, 4, 5, 14, 30, 0, TimeSpan.FromHours(+3)); // MSK
DateTimeOffset хранит точное время и смещение (например, +03:00), не зависит от локального часового пояса и идеален для хранения в БД.
DateTimeOffset now = DateTimeOffset.Now; // Local + offset
DateTimeOffset utcNow = DateTimeOffset.UtcNow; // Utc + 0
TimeSpan представляет интервал времени: разницу между двумя моментами.
DateTime start = DateTime.Now;
// ... работа
DateTime end = DateTime.Now;
TimeSpan duration = end - start;
Console.WriteLine($"Прошло: {duration.TotalSeconds} секунд");
TimeSpan имеет свойства .Days, .Hours, .Minutes, .Seconds, .TotalDays, .TotalHours, .TotalSeconds. Вот как происходит создание:
TimeSpan interval = new TimeSpan(2, 30, 0); // 2 часа 30 минут
TimeSpan.FromHours(1.5); // 1.5 часа
TimeZoneInfo позволяет конвертировать время между часовыми поясами. К примеру, получение часового пояса:
TimeZoneInfo moscow = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time");
TimeZoneInfo paris = TimeZoneInfo.FindSystemTimeZoneById("Romance Standard Time");
Конвертация:
DateTime utcTime = DateTime.UtcNow;
DateTime moscowTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, moscow);
То есть, TimeZoneInfo стоит использовать при работе с пользователями из разных регионов.
Есть и ещё одна тема для изучения - форматирование дат. Основная задача тут это преобразование даты в строку. Для этого используют ToString("format").
Стандартные форматы:
| Формат | Пример | Описание |
|---|---|---|
"d" | 05.04.2025 | Краткая дата |
"D" | Saturday, April 5, 2025 | Полная дата |
"t" | 14:30 | Короткое время |
"T" | 14:30:05 | Полное время |
"f" | April 5, 2025 2:30 PM | Полная дата + короткое время |
"F" | Saturday, April 5, 2025 2:30:05 PM | Полная дата и время |
"g" | 4/5/2025 2:30 PM | Общее короткое |
"G" | 4/5/2025 2:30:05 PM | Общее полное |
"u" | 2025-04-05 14:30:05Z | Универсальное (UTC) |
"o" | 2025-04-05T14:30:05.1234567+03:00 | Round-trip format — точное время с DateTimeOffset |
Пользовательские форматы:
| Спецификатор | Значение | Пример |
|---|---|---|
yyyy | Год (4 цифры) | 2025 |
MM | Месяц (01–12) | 04 |
dd | День (01–31) | 05 |
HH | Часы (00–23) | 14 |
hh | Часы (01–12) | 02 |
mm | Минуты | 30 |
ss | Секунды | 05 |
fff | Миллисекунды | 123 |
z / zz / zzz | Смещение часового пояса (+3, +03, +03:00) | +3, +03:00 |
tt | AM/PM | PM |
DateTime now = DateTime.Now;
string custom = now.ToString("yyyy-MM-dd HH:mm:ss"); // 2025-04-05 14:30:05
А для преобразования строки в дату, используется парсинг строк - DateTime.Parse, TryParse.
DateTime.Parse — выбрасывает исключение при ошибке:
DateTime date = DateTime.Parse("2025-04-05");
DateTime.TryParse — безопасный способ:
if (DateTime.TryParse("2025-04-05", out DateTime date))
{
Console.WriteLine(date);
}
else
{
Console.WriteLine("Неверный формат");
}
Можно выполнять с указанием стиля и культуры:
DateTime.TryParse("05/04/2025", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out date);
В .NET 6+ добавили новые типы для работы только с датой или только со временем - DateOnly и TimeOnly.
DateOnly today = DateOnly.FromDateTime(DateTime.Today);
DateOnly birthday = new(2000, 1, 1);
TimeOnly now = TimeOnly.FromDateTime(DateTime.Now);
TimeOnly lunch = new(12, 30);
Соответственно, они нужны для хранения только даты или только времени.
Итого, для работы можно использовать следующее:
| Метод | Описание | Пример |
|---|---|---|
DateTime.Now | Возвращает текущую дату и время по локальному часовому поясу | var now = DateTime.Now; |
DateTime.UtcNow | Возвращает текущую дату и время в формате UTC | var utc = DateTime.UtcNow; |
date.AddDays(n) | Возвращает новое значение DateTime, с добавленным количеством дней n | var tomorrow = DateTime.Now.AddDays(1); |
date.ToString("format") | Преобразует дату в строку с указанным форматом | now.ToString("yyyy-MM-dd HH:mm:ss") |
DateTime.Parse(str) | Преобразует строку в объект DateTime; выбрасывает исключение при ошибке парсинга | var date = DateTime.Parse("2024-04-05"); |
TimeSpan | Представляет интервал времени (разницу между двумя моментами) | var duration = endDate - startDate; |